'''
Created on Aug 24, 2011

@author: bstar
'''

import logging
import txmongo
from dopplershift.backend.db.mongodb import DSMongodbBackend
from dopplershift.backend.db.txmongoalchemy.query import txQuery, txQueryResult
from mongoalchemy.query_expression import FreeFormDoc
from txmongo._pymongo.objectid import ObjectId
from mongoalchemy.session import Session
from itertools import chain

"""
Monkey patching  mongoalchemy fields for correctly working mapping with 
txMongo driver
"""

from mongoalchemy import fields
fields.ObjectId = ObjectId

logger = logging.getLogger(__name__)

class DSMongoAlchemyBackend(DSMongodbBackend):
    name = "txmongoalchemy"
    def configure(self, config):
        self.queue = []
        self.safe = False
        DSMongodbBackend.configure(self, config)
    def end(self):
        ''' End the session.  Flush all pending operations and ending the 
            *pymongo* request'''
        self.flush()
        self.db.connection.end_request()
    
    def insert(self, item, safe=None):
        ''' Insert an item into the queue and flushes.  Later this function should be smart and delay 
            insertion until the _id field is actually accessed'''
        if safe is None:
            safe = self.safe
        self.queue.append(item)
        self.flush(safe=safe)
    
    def update(self, item, id_expression=None, upsert=False, update_ops={}, safe=None, **kwargs):
        ''' Update an item in the database.  Uses the on_update keyword to each
            field to decide which operations to do, or.  
            
            :param item: An instance of a :class:`~mongoalchemy.document.Document` \
                subclass
            :param id_expression: A query expression that uniquely picks out \
                the item which should be updated.  If id_expression is not \
                passed, update uses item.mongo_id.
            :param upsert: Whether the update operation should be an upsert. \
                If the item may not be in the database yet this should be True
            :param update_ops: By default the operation used to update a field \
                is specified with the on_update argument to its constructor. \
                To override that value, use this dictionary, with  \
                :class:`~mongoalchemy.document.QueryField` objects as the keys \
                and the mongo operation to use as the values.
            :param kwargs: The kwargs are merged into update_ops dict to \
                decide which fields to update the operation for.  These can \
                only be for the top-level document since the keys \
                are just strings.
            
            .. warning::
                
                This operation is **experimental** and **not fully tested**,
                although it does have code coverage.  
            '''
        if id_expression:
            db_key = txQuery(type(item), self).filter(id_expression).query
        else:
            db_key = {'_id' : item.mongo_id}
        
        dirty_ops = item.get_dirty_ops(with_required=upsert)
        for key, op in chain(update_ops.items(), kwargs.items()):
            key = str(key)
            for current_op, keys in dirty_ops.items():
                if key not in keys:
                    continue
                dirty_ops.setdefault(op,{})[key] = keys[key]
                del dirty_ops[current_op][key]
                if len(dirty_ops[current_op]) == 0:
                    del dirty_ops[current_op]
        if safe is None:
            safe = self.safe
        self.flush(safe=safe)
        collection = self.getCollection(item.get_collection_name())
        return collection.update(db_key, dirty_ops)
    def query(self, type):
        ''' Begin a query on the database's collection for `type`.  If `type`
            is an instance of basesting, the query will be in raw query mode
            which will not check field values or transform returned results
            into python objects.
        
         .. seealso:: :class:`~mongoalchemy.query.Query` class'''
        # This really should be adding a query operation to the 
        # queue which is then forced to execute when the results are being
        # read
        if isinstance(type, basestring):
            type = FreeFormDoc(type)
        return txQuery(type, self)
    
    def execute_query(self, query):
        ''' Get the results of ``query``.  This method will flush the queue '''
        collection = self.getCollection(query.type.get_collection_name())
        for index in query.type.get_indexes():
            index.ensure(collection)
        
        kwargs = dict()
        if query.get_fields():
            kwargs['fields'] = [str(f) for f in query.get_fields()]
        zargs = {}
        #f = 
        if query.sort:
            zargs['filter'] = txmongo.filter.sort(txmongo.filter.DESCENDING("created"))
        '''
        cursor = collection.find(query.query, **kwargs)
        
        if query.sort:
            cursor.sort(query.sort)
        if query.hints:
            cursor.hint(query.hints)
        if query.get_limit() != None:
            cursor.limit(query.get_limit())
        if query.get_skip() != None:
            cursor.skip(query.get_skip())
        return QueryResult(cursor, query.type, raw_output=query._raw_output, fields=query.get_fields())
        '''
        
        return self._query_defered(collection, query, zargs)
    def _query_defered(self, collection, query, zargs):
        def generate_query_result(cursor, query):
            try:
                return txQueryResult(cursor, query.type, raw_output=query._raw_output, fields=query.get_fields())
            except:
                print "Not created Query Result"
            return None
        d = collection.find(query.query, **zargs)
        d.addCallback(generate_query_result, query)
        return d
        
    def remove_query(self, type):
        ''' Begin a remove query on the database's collection for `type`.
  
           .. seealso:: :class:`~mongoalchemy.update_expression.RemoveQuery` class'''
        return RemoveQuery(type, self)
    
    def remove(self, obj, safe=None):
        '''
            Remove a particular object from the database.  If the object has 
            no mongo ID set, the method just returns.  If this is a partial 
            document without the mongo ID field retrieved a ``FieldNotRetrieved``
            will be raised
            
            :param obj: the object to save
            :param safe: whether to wait for the operation to complete.  Defaults \
                to the session's ``safe`` value.
        '''
        self.flush()
        if safe is None:
            safe = self.safe
        if not obj.has_id():
            return None
        collection = self.db[obj.get_collection_name()]
        for index in obj.get_indexes():
            index.ensure(collection)
        return self.db[obj.get_collection_name()].remove(obj.mongo_id, safe=safe)
    
    def execute_remove(self, remove):
        ''' Execute a remove expression.  Should generally only be called implicitly.
        '''
        self.flush()
        safe = self.safe
        if remove.safe != None:
            safe = remove.safe
        
        collection = self.db[remove.type.get_collection_name()]
        for index in remove.type.get_indexes():
            index.ensure(collection)

        return self.db[remove.type.get_collection_name()].remove(remove.query, safe=safe)
    
    def execute_update(self, update):
        ''' Execute an update expression.  Should generally only be called implicitly.
        '''
        
        self.flush()
        assert len(update.update_data) > 0
        collection = self.db[update.query.type.get_collection_name()]
        for index in update.query.type.get_indexes():
            index.ensure(collection)
        collection.update(update.query.query, update.update_data, upsert=update.get_upsert(), multi=update.get_multi())
    
    def execute_find_and_modify(self, fm_exp):
        self.flush()
        # assert len(fm_exp.update_data) > 0
        collection = self.db[fm_exp.query.type.get_collection_name()]
        for index in fm_exp.query.type.get_indexes():
            index.ensure(collection)
        kwargs = {
            'query' : fm_exp.query.query, 
            'update' : fm_exp.update_data, 
            'upsert' : fm_exp.get_upsert(), 
        }
        
        if fm_exp.query.get_fields():
            kwargs['fields'] = {}
            for f in fm_exp.query.get_fields():
                kwargs['fields'][str(f)] = True
        if fm_exp.query.sort:
            kwargs['sort'] = fm_exp.query.sort
        if fm_exp.get_new():
            kwargs['new'] = fm_exp.get_new()
        if fm_exp.get_remove():
            kwargs['remove'] = fm_exp.get_remove()
        
        value = collection.find_and_modify(**kwargs)        
        if kwargs['upsert'] and not kwargs.get('new') and len(value) == 0:
            return value
        
        return fm_exp.query.type.unwrap(value, fields=fm_exp.query.get_fields())
    
    def get_indexes(self, cls):
        ''' Get the index information for the collection associated with 
        `cls`.  Index information is returned in the same format as *pymongo*.
        '''
        return self.db[cls.get_collection_name()].index_information()
    
    def clear(self):
        ''' Clear the queue of database operations without executing any of 
             the pending operations'''
        self.queue = []
    
    def clear_collection(self, *classes):
        ''' Clear all objects from the collections associated with the 
            objects in `*cls`. **use with caution!**'''
        for c in classes:
            self.db[c.get_collection_name()].remove()
    
    def flush(self, safe=None):
        ''' Perform all database operations currently in the queue'''
        if safe is None:
            safe = self.safe
        for index, item in enumerate(self.queue):
            item.commit(self.db, safe=safe)
        self.clear()
            
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end()
        return False
